﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Upreader.Application.Model;
using System.Collections.Concurrent;
using System.Linq.Expressions;
using ProtoBuf.Meta;
using System.Threading;

namespace Upreader.Infrastructure.ProtoBuf
{
    /// <summary>
    /// Default implementation for working with the dataContext
    /// </summary>
    public class DataContextModelCollection<TModel> : IModelCollection<TModel> 
        where TModel : ModelBase
    {
        #region Static fields

        private static readonly ConcurrentDictionary<Expression, object> _expressionCache = new ConcurrentDictionary<Expression, object>();

        #endregion // Static fields

        #region Fields

        private readonly DataContext  _dataContext;
        private readonly TypeModel _typeModel;
        private readonly ISet<TModel> _modelSet;
        private readonly Func<int> _idFactory;

        private Dictionary<TModel, ModelState> _changedEntries;

        #endregion // Fields

        #region Constructor

        public DataContextModelCollection(DataContext dataContext, 
            TypeModel typeModel,
            Func<ISet<TModel>> modelSetFactory,
            Func<int> idFactory)
        {
            if (dataContext == null)
                throw new ArgumentNullException("dataContext");
            if (typeModel == null)
                throw new ArgumentNullException("typeModel");
            if (modelSetFactory == null)
                throw new ArgumentNullException("modelSetFactory");
            if (idFactory == null)
                throw new ArgumentNullException("idFactory");

            _dataContext = dataContext;
            _typeModel = typeModel;
            _modelSet = modelSetFactory();
            _idFactory = idFactory;

            _changedEntries = new Dictionary<TModel, ModelState>();
        }

        #endregion // Constructor

        #region Methods

        /// <summary>
        /// Finds all models that satisfy the given predicate in unsorted order
        /// A new colllection will be returned containing copies of all models
        /// </summary>
        public IEnumerable<TModel> Find(Expression<Func<TModel, bool>> predicate)
        {
            // if no predicate, use default predicate
            if (predicate == null)
                predicate = x => true;

            // compile the predicate or get it from the cache
            Func<TModel, bool> compiledPredicate = GetFuncFromExpression(predicate);

            IEnumerable<TModel> localQuery = _changedEntries.Where(x => x.Value == ModelState.Added || x.Value == ModelState.Updated)
                                                            .Select(x => x.Key);

            IEnumerable<TModel> excludeQuery = _changedEntries.Where(x => x.Value == ModelState.Removed).Select(x => x.Key);

            EnterReadLock();
            try
            {
                IEnumerable<TModel> remoteQuery = ModelSet.Where(compiledPredicate)
                                                          .Except(excludeQuery);

                return localQuery.Union(remoteQuery)
                                 .Select(x => DeepClone(x))
                                 .ToArray();
            }
            finally
            {
                ExitReadLock();
            }
        }

        /// <summary>
        /// Adds the given item
        /// </summary>
        public void Add(TModel item)
        {
            if (item == null)
                throw new ArgumentNullException("item");
            if (item.Id != 0)
                throw new ArgumentException("Id of the item is already set. Only Models with Id as 0 can be added");

            // take a write lock since we have to assign a unique id to the item
            EnterWriteLock();
            try
            {
                item.Id = _idFactory();
            }
            finally
            {
                ExitWriteLock();
            }

            _changedEntries[DeepClone(item)] = ModelState.Added;
        }

        public void Update(TModel item)
        {
            if (item == null)
                throw new ArgumentNullException("item");

            // first see if the item exists in the local cache
            ModelState existingState;
            if (_changedEntries.TryGetValue(item, out existingState))
            {
                switch (existingState)
                {
                    case ModelState.Added:
                        break;
                    case ModelState.Updated:
                        // replace the added entity with the updated one but still mark it as added
                        _changedEntries.Remove(item);
                        _changedEntries[item] = ModelState.Added;
                        return;
                    case ModelState.Removed:
                        throw new InvalidOperationException("Cannot update an item which does not exist");
                }
            }

            // secondly, make sure it is also removed in the persisted Set
            EnterReadLock();
            try
            {
                if (ModelSet.Contains(item))
                {
                    _changedEntries[DeepClone(item)] = ModelState.Updated;
                }
                else if (_changedEntries.ContainsKey(item))
                {
                    ModelState currentState = _changedEntries[item];
                    if (currentState == ModelState.Removed)
                    {
                        throw new InvalidOperationException("Cannot update an item which does not exist");
                    }
                    else
                    {
                        // simply readd the item
                        _changedEntries.Remove(item);
                        _changedEntries[DeepClone(item)] = ModelState.Updated;
                    }
                }
                else
                {
                    throw new InvalidOperationException("Cannot update an item which does not exist");
                }
            }
            finally
            {
                ExitReadLock();
            }
        }

        /// <summary>
        /// Marks all models as removed
        /// </summary>
        public void Clear()
        {
            // any pending changes can be ignored
            _changedEntries.Clear();

            EnterReadLock();
            try
            {
                foreach (TModel model in ModelSet)
                {
                    _changedEntries[DeepClone(model)] = ModelState.Removed;
                }
            }
            finally
            {
                ExitReadLock();
            }
        }

        /// <summary>
        /// Determines wether the givem item exists
        /// </summary>
        public bool Contains(TModel item)
        {
            if (item == null)
                throw new ArgumentNullException("item");

            // first see if the item exists in the local cache
            ModelState existingState;
            if (_changedEntries.TryGetValue(item, out existingState))
            {
                if (existingState == ModelState.Added)
                    return true;
            }

            // secondly, search in the underlying data context
            EnterReadLock();
            try
            {
                return ModelSet.Contains(item);
            }
            finally
            {
                ExitReadLock();
            }
        }

        /// <summary>
        /// Copies the entire internal collection to an outstanding array. 
        /// Note that in the current implementation a second copy.
        /// </summary>
        public void CopyTo(TModel[] array, int arrayIndex)
        {
            TModel[] copiedSet = (TModel[])Find(null);

            copiedSet.CopyTo(array, arrayIndex);
        }

        /// <summary>
        /// Marks the item given as removed from the underlying set.
        /// </summary>
        public bool Remove(TModel item)
        {
            if (item == null)
                throw new ArgumentNullException("item");

            // first see if the item exists in the local cache
            ModelState existingState;
            if (_changedEntries.TryGetValue(item, out existingState))
            {
                if (existingState == ModelState.Added)
                {
                    _changedEntries.Remove(item);
                    return true;
                }
            }

            // secondly, make sure it is also removed in the persisted Set
            EnterReadLock();
            try
            {
                if (ModelSet.Contains(item))
                {
                    _changedEntries[DeepClone(item)] = ModelState.Removed;
                    return true;
                }
                else
                {
                    return false;
                }
            }
            finally
            {
                ExitReadLock();
            }
        }

        /// <summary>
        /// Get a enumerator for all models. Note that this method takes a snapshot of the current modelSet
        /// </summary>
        public IEnumerator<TModel> GetEnumerator()
        {
            return Find(null).GetEnumerator();
        }

        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            return (IEnumerator<TModel>)GetEnumerator();
        }

        /// <summary>
        /// Persists all changes to the underlying data context
        /// </summary>
        public IEnumerable<ModelEntry> SaveChanges()
        {
            EnterWriteLock();
            try
            {
                foreach (KeyValuePair<TModel, ModelState> entry in _changedEntries)
                {
                    switch (entry.Value)
                    {
                        case ModelState.Added:
                            _modelSet.Add(entry.Key);
                            break;
                        case ModelState.Updated:
                            // readding this entry will remove the existing one and add the new one
                            _modelSet.Remove(entry.Key);
                            _modelSet.Add(entry.Key);
                            break;
                        case ModelState.Removed:
                            _modelSet.Remove(entry.Key);
                            break;
                    }
                }

                // create a new set of changed entries and use the old one for reporting back
                IEnumerable<ModelEntry> result = _changedEntries.Select(x => new ModelEntry(x.Key.Id, x.Value));

                _changedEntries = new Dictionary<TModel, ModelState>();

                return result;
            }
            finally
            {
                ExitWriteLock();
            }
        }

        #endregion // Methods

        #region Properties

        /// <summary>
        /// Get the total number of models in the current set
        /// </summary>
        public int Count
        {
            get 
            {
                // count locally added entries
                int addedCount = _changedEntries.Count(x => x.Value == ModelState.Added);
                int removedCount = _changedEntries.Count(x => x.Value == ModelState.Removed);

                EnterReadLock();
                
                try
                {
                    int persistedCount = ModelSet.Count;

                    return persistedCount + addedCount - removedCount;
                }
                finally
                {
                    ExitReadLock();
                }
            }
        }

        /// <summary>
        /// Always false
        /// </summary>
        public bool IsReadOnly
        {
            get { return false; }
        }

        #endregion // Properties

        #region Helpers

        protected Func<T, bool> GetFuncFromExpression<T>(Expression<Func<T, bool>> expression)
        {
            object cachedResult;

            if (_expressionCache.TryGetValue(expression, out cachedResult))
            {
                return (Func<T, bool>)cachedResult;
            }
            else
            {
                Func<T, bool> compiledResult = expression.Compile();
                _expressionCache.TryAdd(expression, compiledResult);

                return compiledResult;
            }
        }

        protected void EnterReadLock()
        {
            _dataContext.SynchronizationContext.EnterReadLock();
        }

        protected void ExitReadLock()
        {
            _dataContext.SynchronizationContext.ExitReadLock();
        }

        protected void EnterWriteLock()
        {
            _dataContext.SynchronizationContext.EnterWriteLock();
        }

        protected void ExitWriteLock()
        {
            _dataContext.SynchronizationContext.ExitWriteLock();
        }

        /// <summary>
        /// Get the underlying set from the DataContext relative to this collection
        /// </summary>
        protected ISet<TModel> ModelSet { get { return _modelSet; } }

        /// <summary>
        /// Materializes the given source object to a new copied instance
        /// </summary>
        protected TModel DeepClone(TModel source)
        {
            return (TModel)_typeModel.DeepClone(source);
        }

        #endregion // Helpers
    }
}
